aboutsummaryrefslogtreecommitdiff
path: root/src/app/(main)/websites/[websiteId]/(reports)/breakdown
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/(main)/websites/[websiteId]/(reports)/breakdown')
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx91
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx51
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx46
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx12
4 files changed, 200 insertions, 0 deletions
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx
new file mode 100644
index 0000000..4532d97
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/Breakdown.tsx
@@ -0,0 +1,91 @@
+import { Column, DataColumn, DataTable, Text } from '@umami/react-zen';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { useFields, useFormat, useMessages, useResultQuery } from '@/components/hooks';
+import { formatShortTime } from '@/lib/format';
+
+export interface BreakdownProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ selectedFields: string[];
+}
+
+export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }: BreakdownProps) {
+ const { formatMessage, labels } = useMessages();
+ const { formatValue } = useFormat();
+ const { fields } = useFields();
+ const { data, error, isLoading } = useResultQuery<any>(
+ 'breakdown',
+ {
+ websiteId,
+ startDate,
+ endDate,
+ fields: selectedFields,
+ },
+ { enabled: !!selectedFields.length },
+ );
+
+ return (
+ <LoadingPanel data={data} isLoading={isLoading} error={error}>
+ <Column overflow="auto" minHeight="0" height="100%">
+ <DataTable data={data} style={{ tableLayout: 'fixed' }}>
+ {selectedFields.map(field => {
+ return (
+ <DataColumn
+ key={field}
+ id={field}
+ label={fields.find(f => f.name === field)?.label}
+ width="minmax(120px, 1fr)"
+ >
+ {row => {
+ const value = formatValue(row[field], field);
+ return (
+ <Text truncate title={value}>
+ {value}
+ </Text>
+ );
+ }}
+ </DataColumn>
+ );
+ })}
+ <DataColumn
+ id="visitors"
+ label={formatMessage(labels.visitors)}
+ align="end"
+ width="120px"
+ >
+ {row => row?.visitors?.toLocaleString()}
+ </DataColumn>
+ <DataColumn id="visits" label={formatMessage(labels.visits)} align="end" width="120px">
+ {row => row?.visits?.toLocaleString()}
+ </DataColumn>
+ <DataColumn id="views" label={formatMessage(labels.views)} align="end" width="120px">
+ {row => row?.views?.toLocaleString()}
+ </DataColumn>
+ <DataColumn
+ id="bounceRate"
+ label={formatMessage(labels.bounceRate)}
+ align="end"
+ width="120px"
+ >
+ {row => {
+ const n = (Math.min(row?.visits, row?.bounces) / row?.visits) * 100;
+ return `${Math.round(+n)}%`;
+ }}
+ </DataColumn>
+ <DataColumn
+ id="visitDuration"
+ label={formatMessage(labels.visitDuration)}
+ align="end"
+ width="120px"
+ >
+ {row => {
+ const n = row?.totaltime / row?.visits;
+ return `${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`;
+ }}
+ </DataColumn>
+ </DataTable>
+ </Column>
+ </LoadingPanel>
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
new file mode 100644
index 0000000..fdead9f
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage.tsx
@@ -0,0 +1,51 @@
+'use client';
+import { Column, Row } from '@umami/react-zen';
+import { useState } from 'react';
+import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm';
+import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
+import { Panel } from '@/components/common/Panel';
+import { useDateRange, useMessages } from '@/components/hooks';
+import { ListCheck } from '@/components/icons';
+import { DialogButton } from '@/components/input/DialogButton';
+import { Breakdown } from './Breakdown';
+
+export function BreakdownPage({ websiteId }: { websiteId: string }) {
+ const {
+ dateRange: { startDate, endDate },
+ } = useDateRange();
+ const [fields, setFields] = useState(['path']);
+ return (
+ <Column gap>
+ <WebsiteControls websiteId={websiteId} />
+ <Row alignItems="center" justifyContent="flex-start">
+ <FieldsButton value={fields} onChange={setFields} />
+ </Row>
+ <Panel height="900px" overflow="auto" allowFullscreen>
+ <Breakdown
+ websiteId={websiteId}
+ startDate={startDate}
+ endDate={endDate}
+ selectedFields={fields}
+ />
+ </Panel>
+ </Column>
+ );
+}
+
+const FieldsButton = ({ value, onChange }) => {
+ const { formatMessage, labels } = useMessages();
+
+ return (
+ <DialogButton
+ icon={<ListCheck />}
+ label={formatMessage(labels.fields)}
+ width="400px"
+ minHeight="300px"
+ variant="outline"
+ >
+ {({ close }) => {
+ return <FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />;
+ }}
+ </DialogButton>
+ );
+};
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx
new file mode 100644
index 0000000..28e3368
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/FieldSelectForm.tsx
@@ -0,0 +1,46 @@
+import { Button, Column, Grid, List, ListItem } from '@umami/react-zen';
+import { useState } from 'react';
+import { useFields, useMessages } from '@/components/hooks';
+
+export function FieldSelectForm({
+ selectedFields = [],
+ onChange,
+ onClose,
+}: {
+ selectedFields?: string[];
+ onChange: (values: string[]) => void;
+ onClose?: () => void;
+}) {
+ const [selected, setSelected] = useState(selectedFields);
+ const { formatMessage, labels } = useMessages();
+ const { fields } = useFields();
+
+ const handleChange = (value: string[]) => {
+ setSelected(value);
+ };
+
+ const handleApply = () => {
+ onChange?.(selected);
+ onClose();
+ };
+
+ return (
+ <Column gap="6">
+ <List value={selected} onChange={handleChange} selectionMode="multiple">
+ {fields.map(({ name, label }) => {
+ return (
+ <ListItem key={name} id={name}>
+ {label}
+ </ListItem>
+ );
+ })}
+ </List>
+ <Grid columns="1fr 1fr" gap>
+ <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
+ <Button onPress={handleApply} variant="primary">
+ {formatMessage(labels.apply)}
+ </Button>
+ </Grid>
+ </Column>
+ );
+}
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx
new file mode 100644
index 0000000..841d863
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/breakdown/page.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from 'next';
+import { BreakdownPage } from './BreakdownPage';
+
+export default async function ({ params }: { params: Promise<{ websiteId: string }> }) {
+ const { websiteId } = await params;
+
+ return <BreakdownPage websiteId={websiteId} />;
+}
+
+export const metadata: Metadata = {
+ title: 'Insights',
+};